A list is the right starting point because it's the smallest complete loop the engine runs: SQL goes in, a row template gets repeated once per result, HTML comes out. Nothing else on the platform is simpler than this, and almost everything else is built from the same idea.
The component for this is dynamiclist — a repeating HTML-style structure driven by data. In XML, it appears as an element where you provide an outer container tag, a per-row template, and a SQL query. The engine runs the query and, for every row returned, renders your template once with that row's values substituted in.
Here's a complete, working dynamiclist block for a Contacts list — nothing trimmed:
<Block tag='div'> <Element type='dynamiclist' tag='div'> <Attributes class='contact-grid' /> <ListRow tag='div'> <Attributes class='contact-card' /> <Element type='htmlitem' tag='div'> <Attributes class='contact-avatar' /> <Text>%initials%</Text> </Element> <Element type='inlinecontainer' tag='div'> <Element type='htmlitem' tag='div'> <Attributes class='contact-name' /> <Text>%contact_name%</Text> </Element> <Element type='htmlitem' tag='div'> <Attributes class='contact-role' /> <Text>%job_title%</Text> </Element> <Element type='htmlitem' tag='div'> <Attributes class='contact-meta' /> <Text>%email%</Text> </Element> </Element> </ListRow> <!-- SQL always comes last, after ListRow --> <SQL> <query name='contacts_list'> <![CDATA[ SELECT contact_id, contact_name, job_title, email, UPPER(LEFT(contact_name,1)) AS initials FROM contacts WHERE dept_id = '%deptid%' ORDER BY contact_name ASC ]]> </query> </SQL> </Element> </Block>
That's the whole thing. No loop to write, no result-set iteration, no string concatenation to build up a row of markup — just a template and a query.
What each piece is doing
<Block>, not the dynamiclist itself — the actual list content sits one level in, as the Block's child.contact-grid class.ListRow only — they don't leak into the rest of the page the way a single-row block query would.dynamiclist, the query always comes after ListRow — the same file-ordering convention used everywhere else on the platform.The engine runs contacts_list, gets back however many rows match, and renders ListRow once per row with that row's tokens substituted. For four contacts, the output looks like this:
<div class="contact-grid"> <div class="contact-card"> <div class="contact-avatar">S</div> <div> <div class="contact-name">Sarah Whitfield</div> <div class="contact-role">Operations Manager</div> <div class="contact-meta">s.whitfield@example.com</div> </div> </div> <!-- … repeated once per row, no two the same — the template is fixed, the data isn't … --> </div>
Every card is the identical markup structure — only the token values change. That's the entire value of a dynamiclist: write the shape once, let row count vary freely.
And here's that generated HTML, live, in a real browser — not a mockup:
Why not just write a PHP loop?
Why should you? Because doing this manually means backend code, escaping values, wiring templates, and keeping everything in sync — work you don't need to do. dynamiclist lets you define the structure and the SQL together, and the engine handles the repetitive part.
In the next post, we'll look at dynamiclisttable — the component built specifically for tabular, column-based data — and how it extends the same idea for spreadsheet-like layouts.
Building a Table: sortable, structured data with dynamiclisttable
Post 4 takes the same Contacts data and puts it in a proper structured table — columns, headers, and the platform's built-in sorting.